﻿// <copyright file="PerspectiveCamera.cs" company="Axelerate Solutions S.A.">
// Copyright (c) Axelerate Solutions S.A.  All rights reserved.
// </copyright>
namespace Axelerate.Silverlight3D.Media.Media3D
{
    using System;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Ink;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Shapes;

    /// <summary>
    /// File name: PerspectiveCamera.cs
    /// Company: Axelerate Solutions S.A.
    /// Created By: Daniel Rojas
    /// Created Date: 10/13/2008 9:11:00 h.
    /// Description: Represents a perspective projection camera.
    ///   <Modifications>
    ///   </Modifications>
    /// </summary>
    public sealed class PerspectiveCamera : ProjectionCamera
    {
        #region Fields
        /// <summary>
        /// Identifies the FieldOfView dependency property.
        /// </summary>
        public static readonly DependencyProperty FieldOfViewProperty =
                    DependencyProperty.Register(
                          "FieldOfView",
                          typeof(double),
                          typeof(PerspectiveCamera),
                          new PropertyMetadata(OnFieldOfViewPropertyChanged));

        /// <summary>
        /// cache variable to store the world to screen matrix
        /// </summary>
        private Matrix3D _cachedWorldToScreenMatrix;

        /// <summary>
        /// cache variable to store the screen to view matrix
        /// </summary>
        private Matrix3D _cachedScreenToViewMatrix;

        /// <summary>
        /// cache variable to store the view to world matrix
        /// </summary>
        private Matrix3D _cachedViewToWorldMatrix;
        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a value that represents the camera's horizontal field of view.
        /// </summary>
        public double FieldOfView
        {
            get
            {
                return (double)this.GetValue(FarPlaneDistanceProperty);
            }
            set
            {
                this.SetValue(FarPlaneDistanceProperty, value);
            }
        }

        /// <summary>
        /// Gets the WorldToScreen Matrix transformation
        /// </summary>
        internal override Matrix3D WorldToScreenMatrix
        {
            get
            {
                if (this.IsDirty)
                {
                    this.CalculateScreenMatrixes();
                }
                return this._cachedWorldToScreenMatrix;
            }
        }

        /// <summary>
        /// Gets the ScreenToViewMatrix Transformation
        /// </summary>
        internal override Matrix3D ScreenToViewMatrix
        {
            get
            {
                if (this.IsDirty)
                {
                    this.CalculateScreenMatrixes();
                }
                return this._cachedScreenToViewMatrix;
            }
        }

        /// <summary>
        /// Gets the ViewToWorld Matrix transformation
        /// </summary>
        internal override Matrix3D ViewToWorldMatrix
        {
            get
            {
                if (this.IsDirty)
                {
                    this.CalculateScreenMatrixes();
                }
                return this._cachedViewToWorldMatrix;
            }
        }
        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the PerspectiveCamera class.
        /// </summary>
        public PerspectiveCamera()
            : base()
        {
            this.Position = new Point3D(0, 0, 0);
            this.LookDirection = new Vector3D(0, 0, 1);
            this.UpDirection = new Vector3D(0, 1, 0);
            this.FieldOfView = 45.0f;
            this.IsDirty = true;
        }

        /// <summary>
        /// Initializes a new instance of the PerspectiveCamera class using the specified position, direction, and field of view.
        /// </summary>
        /// <param name="position">Point3D that specifies the camera's position.</param>
        /// <param name="lookDirection">Vector3D that specifies the direction of the camera's projection.</param>
        /// <param name="upDirection">Vector3D that specifies the upward direction according to the perspective of the onlooker.</param>
        /// <param name="fieldOfView">Width of the camera's angle of projection, specified in degrees.</param>
        public PerspectiveCamera(Point3D position, Vector3D lookDirection, Vector3D upDirection, double fieldOfView)
            : base()
        {
            this.Position = position;
            this.LookDirection = lookDirection;
            this.UpDirection = upDirection;
            this.FieldOfView = fieldOfView;
            this.IsDirty = true;
        }

        #endregion

        #region Methods
        /// <summary>
        /// Raise the FieldOfView changed event
        /// </summary>
        /// <param name="d">DependencyObject owner of the property</param>
        /// <param name="e">DependencyPropertyChangedEventArgs with the necesary information</param>
        private static void OnFieldOfViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((PerspectiveCamera)d).NotifyPropertyChanged("FieldOfView");
            ((PerspectiveCamera)d).IsDirty = true;
        }

        /// <summary>
        /// Calculate the matrixes to chached it.
        /// </summary>
        private void CalculateScreenMatrixes()
        {
            if ((this.parentViewport == null) ||
                (this.parentViewport.ActualHeight == 0) ||
                (this.parentViewport.ActualWidth == 0))
            {
                return;
            }

            // transform the vectors of the camera to the current tranformation
            Vector3D lookDirection = this.LookDirection;
            if (this.Transform != null)
            {
                lookDirection = this.Transform.Transform(lookDirection);
            }
            Vector3D cameraZAxis = -lookDirection;
            cameraZAxis.Normalize();

            Vector3D upDirection = this.UpDirection;
            if (this.Transform != null)
            {
                upDirection = this.Transform.Transform(upDirection);
            }
            Vector3D cameraXAxis = Vector3D.CrossProduct(this.UpDirection, cameraZAxis);
            cameraXAxis.Normalize();

            // No need to normalize since both vectors used are normalized
            Vector3D cameraYAxis = Vector3D.CrossProduct(cameraZAxis, cameraXAxis);

            Point3D position = this.Position;
            if (this.Transform != null)
            {
                position = this.Transform.Transform(position);
            }
            Vector3D cameraPosition = (Vector3D)position;
            double offsetX = -Vector3D.DotProduct(cameraXAxis, cameraPosition);
            double offsetY = -Vector3D.DotProduct(cameraYAxis, cameraPosition);
            double offsetZ = -Vector3D.DotProduct(cameraZAxis, cameraPosition);

            Matrix3D viewMatrix = new Matrix3D(cameraXAxis.X, cameraYAxis.X, cameraZAxis.X, 0,
                                               cameraXAxis.Y, cameraYAxis.Y, cameraZAxis.Y, 0,
                                               cameraXAxis.Z, cameraYAxis.Z, cameraZAxis.Z, 0,
                                               offsetX, offsetY, offsetZ, 1);

            // Now we can create the projection matrix
            double aspectRatio = this.parentViewport.ActualWidth / this.parentViewport.ActualHeight;
            double xScale = 1.0 / System.Math.Tan(System.Math.PI * this.FieldOfView / 360);
            double yScale = aspectRatio * xScale;
            double zScale = (this.FarPlaneDistance == double.PositiveInfinity) ? -1 : this.FarPlaneDistance / (this.NearPlaneDistance - this.FarPlaneDistance);
            double zOffset = this.NearPlaneDistance * zScale;


            Matrix3D projection = new Matrix3D(xScale, 0, 0, 0,
                                               0, yScale, 0, 0,
                                               0, 0, zScale, -1,
                                               0, 0, zOffset, 0);

            // Create the NDC to screen matrix, know NDC is kept at 2 units high
            // y' = - (hs / 2) * Yndc + hs/2 + sx
            // x' = (ws / 2) * Xndc + ws/2 + sy
            // z' = (ds / 2) * Zndc + ds/2   //ds is [0,1] where our ndc z was from -1 to 1
            double screenDepth = 1;
            Matrix3D ndcToScreen = new Matrix3D(this.parentViewport.ActualWidth / 2, 0, 0, 0,
                                                0, -this.parentViewport.ActualHeight / 2, 0, 0,
                                                0, 0, screenDepth / 2, 0,
                                                this.parentViewport.ActualWidth / 2, this.parentViewport.ActualHeight / 2, screenDepth / 2, 1);

            // Need to define the total overall transformation
            // model->world->view->projection->screen
            this._cachedWorldToScreenMatrix = viewMatrix * projection * ndcToScreen;

            // Store the transform that goes from view space to world space
            Matrix3D viewToWorldTransform = viewMatrix;
            viewToWorldTransform.Invert();
            this._cachedViewToWorldMatrix = viewToWorldTransform;

            Matrix3D ScreenToView = this._cachedWorldToScreenMatrix;
            if (ScreenToView.HasInverse)
            {
                ScreenToView.Invert();
                this._cachedScreenToViewMatrix = ScreenToView;
            }

            this.IsDirty = false;
        }
        #endregion

    }
}
